Типы ошибок в GraphQL
В GraphQL можно столкнуться с тремя основными типами ошибок:
- Ошибки запроса (GraphQL Errors) — возникают, если сервер GraphQL вернул данные с ошибкой (например, вместо
products, которые пользователь запросил, вернулсяnullс сообщением "Unauthorized"). - Сетевые ошибки (Network Errors) — появляются, если запрос не смог вообще достучаться до сервера, например, из-за сбоя сети или недоступности сервера.
- Локальные ошибки (Local Errors) — это ошибки, возникающие в вашем приложении, скажем, из-за неправильной обработки данных или опечатки. Да-да, ошибки в коде — наше всё!
Управление состоянием загрузки и ошибок с Apollo Client
Apollo Client реализует встроенные механизмы для управления состояниями загрузки и ошибок, которые возвращаются из хуков useQuery и useMutation. Они дают вам полное представление о том, что происходит с запросами, напрямую через возвращаемые значения.
Пример состояния загрузки
Создаём запрос с использованием useQuery:
import { useQuery, gql } from '@apollo/client';
const GET_PRODUCTS = gql`
query GetProducts {
products {
id
name
price
}
}
`;
const ProductList: React.FC = () => {
const { loading, error, data } = useQuery(GET_PRODUCTS);
if (loading) return <p>Загрузка...</p>;
if (error) return <p>Ошибка: {error.message}</p>;
return (
<ul>
{data.products.map((product: { id: string; name: string; price: number }) => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
);
};
Обратите внимание на три ключевых состояния:
loading—true, пока данные загружаются.error— содержит объект ошибки, если запрос завершился неудачно.data— содержит ответ сервера, если запрос прошёл успешно.
Вот так легко мы добавили индикатор загрузки и обработку ошибок.
Более изящная обработка ошибок
Если просто вывести сообщение об ошибке — это неплохо, но чаще всего пользователям нужен дружелюбный и понятный интерфейс. Например:
if (loading) {
return <p>Подождите, грузим ваши продукты...</p>;
}
if (error) {
return (
<div>
<h2>Что-то пошло не так 😢</h2>
<p>Подробности: {error.message}</p>
<button onClick={() => window.location.reload()}>Попробовать снова</button>
</div>
);
}
Если ошибка всё же произошла, мы предлагаем пользователю кнопку "Попробовать снова".
Локализация обработки ошибок
Часто ошибки могут повторяться в разных местах приложения. Чтобы не писать одно и то же снова и снова, можно вынести обработку ошибок в отдельный компонент или хук.
Создание пользовательского хука для обработки ошибок
import { ApolloError } from '@apollo/client';
const useHandleError = (error: ApolloError | undefined) => {
if (!error) return null;
// Пример: Логирование ошибок
console.error('GraphQL Error:', error);
// Можно добавить кастомную обработку
if (error.networkError) {
return 'Проблема с соединением. Проверьте интернет.';
}
if (error.graphQLErrors.length > 0) {
return error.graphQLErrors[0].message;
}
return 'Неизвестная ошибка';
};
export default useHandleError;
Теперь вы можете использовать этот хук в вашем компоненте:
const errorMessage = useHandleError(error);
if (errorMessage) {
return <p>{errorMessage}</p>;
}
Управление состоянием загрузки
Индикаторы загрузки помогают создать ощущение "живого" приложения. Но они должны быть не только информативными, но и ненавязчивыми.
Можно использовать готовую библиотеку (например, react-spinners) или создать свой простой компонент для отображения спиннера:
const Spinner: React.FC = () => (
<div className="spinner">
<div className="loader" />
<style jsx>{`
.spinner {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.loader {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`}</style>
</div>
);
Используйте его во время загрузки:
if (loading) {
return <Spinner />;
}
Практическое применение
Давайте "усложним". Сделаем компонент, который обрабатывает ошибки и загрузку универсально:
const LoadingErrorWrapper: React.FC<{ loading: boolean; error: ApolloError | undefined }> = ({ loading, error, children }) => {
const errorMessage = useHandleError(error);
if (loading) {
return <Spinner />;
}
if (errorMessage) {
return <p>{errorMessage}</p>;
}
return <>{children}</>;
};
Используем наш компонент:
<LoadingErrorWrapper loading={loading} error={error}>
<ul>
{data.products.map((product: { id: string; name: string; price: number }) => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
</LoadingErrorWrapper>
Теперь управление состояниями загрузки и ошибок стало максимально простым!
Типичные ошибки
- Не обрабатывать все состояния (например,
loadingесть, аerrorзабыли). - Показывать пользователю незавершённую загрузку, не добавив индикатор.
- Оставлять ошибки без перевода/локализации: "Cannot fetch data" для пользователя выглядит странно, если ваш интерфейс не на английском.
- Неочевидные действия: пользователю предлагается что угодно, но не кнопка для повторной попытки.
Итак, теперь вы знаете, как создавать отзывчивые и устойчивые к ошибкам интерфейсы. Как разработчик, ваше приложение стало умнее и лучше взаимодействует с пользователем!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ