Храните поиск и страницу в состоянии, загружайте данные с отменой устаревшего запроса. Сортируйте полученную страницу, а подробности показывайте в диалоге.
React‑приложение со списком персонажей
Храните поиск и страницу в состоянии, загружайте данные с отменой устаревшего запроса. Сортируйте полученную страницу, а подробности показывайте в диалоге.
Короткий ответ
Что ответить на собеседовании
Подробный разбор
Ответ с пояснениями
Условие
Разработать React‑приложение, которое получает перечень персонажей из публичного API «Rick and Morty». В интерфейсе необходимо реализовать:
- поле поиска и возможность сортировки по имени и статусу;
- постраничный вывод данных;
- для каждого элемента списка показывать всплывающее окно с подробной информацией о персонаже.
Константа с адресом API:
API_LINK = "https://rickandmortyapi.com/api/character";
Базовый шаблон приложения (не менять):
import "./styles.css";
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>hello</h2>
</div>
);
Ответ
Храните поиск и страницу в состоянии, загружайте данные с отменой устаревшего запроса. Сортируйте полученную страницу, а подробности показывайте в диалоге.
API поддерживает name и page, но не глобальную сортировку по имени или статусу. Ниже сортируется текущая страница; глобальный порядок потребует отдельного слоя данных. Сохраните обёртку и заголовки App, добавьте внутрь <Characters /> и недостающую закрывающую скобку функции.
import { useEffect, useRef, useState } from 'react';
const API_LINK = 'https://rickandmortyapi.com/api/character';
function Characters() {
const [query, setQuery] = useState({ name: '', page: 1 });
const [sort, setSort] = useState('name');
const [data, setData] = useState({ results: [], pages: 0 });
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [selected, setSelected] = useState(null);
const dialog = useRef(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError('');
const timer = setTimeout(async () => {
try {
const url = new URL(API_LINK);
url.searchParams.set('name', query.name);
url.searchParams.set('page', String(query.page));
const response = await fetch(url, { signal: controller.signal });
if (response.status === 404) {
if (!controller.signal.aborted) setData({ results: [], pages: 0 });
return;
}
if (!response.ok) throw new Error('Не удалось загрузить персонажей');
const payload = await response.json();
if (!controller.signal.aborted) {
setData({ results: payload.results, pages: payload.info.pages });
}
} catch (e) {
if (!controller.signal.aborted) setError(e.message);
} finally {
if (!controller.signal.aborted) setLoading(false);
}
}, 250);
return () => { clearTimeout(timer); controller.abort(); };
}, [query]);
useEffect(() => {
if (selected && !dialog.current.open) dialog.current.showModal();
}, [selected]);
const items = [...data.results].sort((a, b) => a[sort].localeCompare(b[sort]));
return <section>
<label>Поиск <input value={query.name} onChange={e =>
setQuery({ name: e.target.value, page: 1 })} /></label>
<label>Сортировка <select value={sort} onChange={e => setSort(e.target.value)}>
<option value="name">Имя</option><option value="status">Статус</option>
</select></label>
{loading ? <p role="status">Загрузка…</p> : error ? <p role="alert">{error}</p> : <>
{!items.length && <p>Ничего не найдено</p>}
<ul>{items.map(item => <li key={item.id}>
<button onClick={() => setSelected(item)}>{item.name} — {item.status}</button>
</li>)}</ul>
</>}
<button disabled={loading || query.page <= 1} onClick={() =>
setQuery(q => ({ ...q, page: q.page - 1 }))}>Назад</button>
<span> Страница {query.page} / {data.pages || 1} </span>
<button disabled={loading || query.page >= data.pages} onClick={() =>
setQuery(q => ({ ...q, page: q.page + 1 }))}>Вперёд</button>
<dialog ref={dialog} aria-labelledby="character-name" onClose={() => setSelected(null)}>
{selected && <><h2 id="character-name">{selected.name}</h2>
<img src={selected.image} alt={selected.name} width="160" />
<p>{selected.status} · {selected.species} · {selected.gender}</p>
<p>Происхождение: {selected.origin.name}</p>
<p>Локация: {selected.location.name}</p></>}
<button onClick={() => dialog.current.close()}>Закрыть</button>
</dialog>
</section>;
}
Источник: rickandmortyapi.com.