Использование с Suspense и ErrorBoundary

Библиотека Nivo активно использует React-компоненты и хорошо сочетается с современными механизмами асинхронного рендеринга. При работе с удалёнными источниками данных, ленивой загрузкой графиков и динамическими dashboard-интерфейсами особенно важны:

  • корректная обработка состояний загрузки;
  • защита интерфейса от ошибок визуализации;
  • разделение тяжёлых компонентов на отдельные чанки;
  • предотвращение полного падения страницы из-за сбоя одного графика.

Для решения этих задач используются:

  • React.Suspense;
  • React.lazy;
  • ErrorBoundary.

Проблемы асинхронной загрузки графиков

Компоненты Nivo могут:

  • загружать большие объёмы данных;
  • использовать тяжёлые SVG/Canvas-вычисления;
  • динамически импортироваться;
  • зависеть от API-запросов;
  • падать при некорректной структуре данных.

Без изоляции ошибок возникают типичные проблемы:

<ResponsiveBar data={undefined} />

Результат:

TypeError: Cannot read property 'map' of undefined

Ошибка внутри одного графика способна уничтожить весь React tree.


Suspense: базовый принцип работы

Suspense позволяет показывать fallback-интерфейс во время:

  • динамического импорта;
  • ожидания данных;
  • lazy-загрузки компонентов.

Простейшая схема:

<Suspense fallback={<Loader />}>
    <Chart />
</Suspense>

Пока компонент загружается — отображается Loader.


Lazy loading графиков Nivo

Графики Nivo достаточно тяжёлые. Особенно:

  • ResponsiveChoropleth;
  • ResponsiveNetwork;
  • ResponsiveSunburst;
  • ResponsiveTreeMap.

Поэтому эффективной практикой является разделение bundle через React.lazy.


Установка библиотек

npm install @nivo/bar

Динамический импорт графика

import React, { lazy, Suspense } from 'react'

const BarChart = lazy(() =>
    import('./BarChart')
)

export default function App() {
    return (
        <Suspense fallback={<div>Загрузка графика...</div>}>
            <BarChart />
        </Suspense>
    )
}

Структура компонента графика

import { ResponsiveBar } from '@nivo/bar'

const data = [
    {
        country: 'USA',
        value: 120
    },
    {
        country: 'Germany',
        value: 90
    }
]

export default function BarChart() {
    return (
        <div style={{ height: 400 }}>
            <ResponsiveBar
                data={data}
                keys={['value']}
                indexBy="country"
            />
        </div>
    )
}

Что происходит во время lazy loading

React:

  1. доходит до BarChart;
  2. видит динамический import;
  3. приостанавливает рендер;
  4. показывает fallback;
  5. загружает JS chunk;
  6. продолжает рендер.

Преимущества lazy loading для Nivo

Снижение initial bundle size

Особенно критично для dashboard-приложений.

Без lazy loading:

main.js = 2.4 MB

С lazy loading:

main.js = 620 KB

Быстрее первый рендер

Интерфейс становится интерактивным раньше.


Независимая загрузка графиков

Каждый chart может подгружаться отдельно.


Использование нескольких Suspense

Каждый график желательно изолировать.


Плохая практика

<Suspense fallback={<PageLoader />}>
    <SalesChart />
    <UsersChart />
    <RevenueChart />
</Suspense>

Если один график грузится долго — блокируются все.


Хорошая практика

<>
    <Suspense fallback={<ChartLoader />}>
        <SalesChart />
    </Suspense>

    <Suspense fallback={<ChartLoader />}>
        <UsersChart />
    </Suspense>

    <Suspense fallback={<ChartLoader />}>
        <RevenueChart />
    </Suspense>
</>

Специализированный fallback для графиков

Обычный текст выглядит плохо.


Skeleton loader

function ChartSkeleton() {
    return (
        <div
            style={{
                height: 400,
                borderRadius: 12,
                background: '#f3f3f3'
            }}
        />
    )
}

Использование

<Suspense fallback={<ChartSkeleton />}>
    <BarChart />
</Suspense>

ErrorBoundary: защита от падения графиков

Nivo ожидает корректные данные.

Ошибки возникают при:

  • null;
  • undefined;
  • неверных keys;
  • пустых массивах;
  • несовместимых форматах;
  • ошибках Canvas/SVG.

Базовый ErrorBoundary

import React from 'react'

export default class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props)

        this.state = {
            hasError: false
        }
    }

    static getDerivedStateFromError() {
        return {
            hasError: true
        }
    }

    componentDidCatch(error, info) {
        console.error(error)
        console.error(info)
    }

    render() {
        if (this.state.hasError) {
            return (
                <div>
                    Ошибка отображения графика
                </div>
            )
        }

        return this.props.children
    }
}

Изоляция Nivo-компонента

<ErrorBoundary>
    <ResponsiveBar {...props} />
</ErrorBoundary>

Теперь сбой графика не уничтожит приложение.


Комбинирование Suspense и ErrorBoundary

Наиболее распространённая архитектура:

<ErrorBoundary>
    <Suspense fallback={<ChartSkeleton />}>
        <SalesChart />
    </Suspense>
</ErrorBoundary>

Порядок вложенности

Правильный порядок:

<ErrorBoundary>
    <Suspense>
        <Chart />
    </Suspense>
</ErrorBoundary>

Почему именно так

ErrorBoundary должен перехватывать:

  • ошибки рендера;
  • ошибки lazy import;
  • ошибки Nivo;
  • ошибки внутри chart logic.

Если разместить наоборот:

<Suspense>
    <ErrorBoundary>
        <Chart />
    </ErrorBoundary>
</Suspense>

часть ошибок останется непойманной.


Обработка ошибок API

Частая ситуация:

const response = await fetch('/api/chart')

API возвращает некорректные данные.


Потенциальная проблема

[
    {
        amount: 120
    }
]

Но график ожидает:

[
    {
        country: 'USA',
        amount: 120
    }
]

Nivo может выбросить runtime exception.


Валидация данных перед рендером

function validateData(data) {
    return data.every(item =>
        item.country &&
        typeof item.amount === 'number'
    )
}

Использование

if (!validateData(data)) {
    return <div>Некорректные данные</div>
}

Асинхронная загрузка данных и Suspense

С React 18 появились patterns для data fetching через Suspense.


Пример resource wrapper

function createResource(promise) {
    let status = 'pending'
    let result

    const suspender = promise.then(
        r => {
            status = 'success'
            result = r
        },
        e => {
            status = 'error'
            result = e
        }
    )

    return {
        read() {
            if (status === 'pending') {
                throw suspender
            }

            if (status === 'error') {
                throw result
            }

            return result
        }
    }
}

Создание ресурса

const chartResource = createResource(
    fetch('/api/chart')
        .then(r => r.json())
)

Использование внутри графика

function Chart() {
    const data = chartResource.read()

    return (
        <ResponsiveBar
            data={data}
            keys={['value']}
            indexBy="country"
        />
    )
}

Интеграция с Suspense

<ErrorBoundary>
    <Suspense fallback={<ChartSkeleton />}>
        <Chart />
    </Suspense>
</ErrorBoundary>

Как работает Suspense с ошибками

Если promise:

  • pending → показывается fallback;
  • resolved → рендерится график;
  • rejected → ошибка передаётся в ErrorBoundary.

Повторная попытка рендера

ErrorBoundary можно сбрасывать.


Пример reset logic

class ErrorBoundary extends React.Component {
    state = {
        hasError: false
    }

    static getDerivedStateFromError() {
        return {
            hasError: true
        }
    }

    reset = () => {
        this.setState({
            hasError: false
        })
    }

    render() {
        if (this.state.hasError) {
            return (
                <div>
                    <p>Ошибка графика</p>

                    <button onCl ick={this.reset}>
                        Повторить
                    </button>
                </div>
            )
        }

        return this.props.children
    }
}

Использование react-error-boundary

Самостоятельная реализация не всегда удобна.

Популярное решение:

npm install react-error-boundary

Базовое использование

import { ErrorBoundary } from 'react-error-boundary'

Компонент fallback

function ErrorFallback({ error, resetErrorBoundary }) {
    return (
        <div>
            <p>{error.message}</p>

            <button onCl ick={resetErrorBoundary}>
                Повторить
            </button>
        </div>
    )
}

Интеграция

<ErrorBoundary
    FallbackComponent={ErrorFallback}
>
    <Suspense fallback={<ChartSkeleton />}>
        <Chart />
    </Suspense>
</ErrorBoundary>

Retry при повторном запросе

<ErrorBoundary
    FallbackComponent={ErrorFallback}
    onRe set={() => {
        reloadChartData()
    }}
>
    <Chart />
</ErrorBoundary>

Canvas-графики и ErrorBoundary

Canvas-версии Nivo:

  • ResponsiveBarCanvas;
  • ResponsiveLineCanvas;
  • ResponsiveScatterPlotCanvas;

чаще вызывают runtime errors при:

  • нехватке памяти;
  • слишком больших dataset;
  • invalid dimensions.

Изоляция Canvas-графиков

<ErrorBoundary>
    <ResponsiveLineCanvas {...props} />
</ErrorBoundary>

Это особенно важно для аналитических систем.


Suspense и SSR

При использовании:

  • Next.js;
  • Remix;
  • React Server Components;

Suspense становится частью серверного рендеринга.


Next.js и динамический импорт

import dynamic from 'next/dynamic'

const Chart = dynamic(
    () => import('./Chart'),
    {
        ssr: false,
        loading: () => <ChartSkeleton />
    }
)

Почему SSR может ломать Nivo

Некоторые компоненты используют:

window
document
ResizeObserver

На сервере этих объектов нет.


SSR-safe загрузка

const Chart = dynamic(
    () => import('./Chart'),
    {
        ssr: false
    }
)

Комбинация dynamic и ErrorBoundary

<ErrorBoundary>
    <Chart />
</ErrorBoundary>

Разделение dashboard по boundaries

Крупные dashboard лучше сегментировать.


Пример архитектуры

<Dashboard>

    <ErrorBoundary>
        <SalesCharts />
    </ErrorBoundary>

    <ErrorBoundary>
        <MarketingCharts />
    </ErrorBoundary>

    <ErrorBoundary>
        <FinanceCharts />
    </ErrorBoundary>

</Dashboard>

Преимущества сегментации

При сбое:

  • ломается только один раздел;
  • остальные графики продолжают работать;
  • dashboard остаётся интерактивным.

Логирование ошибок Nivo

ErrorBoundary позволяет подключать:

  • Sentry;
  • LogRocket;
  • Datadog;
  • Bugsnag.

Пример с Sentry

componentDidCatch(error, info) {
    Sentry.captureException(error, {
        extra: info
    })
}

Типичные ошибки Nivo

Некорректный keys

keys={['revenue']}

Но данные:

{
    sales: 120
}

Undefined data

data={undefined}

Ошибки размеров контейнера

<div>
    <ResponsiveBar />
</div>

Без высоты:

height: 0

Безопасный контейнер

<div
    style={{
        height: 400
    }}
>
    <ResponsiveBar />
</div>

Graceful degradation

Иногда вместо падения лучше отображать резервный UI.


Пример fallback chart

function EmptyChartState() {
    return (
        <div>
            Данные временно недоступны
        </div>
    )
}

Использование

<ErrorBoundary
    fallback={<EmptyChartState />}
>
    <Chart />
</ErrorBoundary>

Предотвращение лишних Suspense

Плохая практика:

<Suspense>
    <ChartPart1 />
</Suspense>

<Suspense>
    <ChartPart2 />
</Suspense>

<Suspense>
    <ChartPart3 />
</Suspense>

Возникают:

  • множественные fallback;
  • мерцания;
  • layout shift.

Оптимальный уровень Suspense

Лучше оборачивать:

  • целый chart;
  • dashboard section;
  • tab panel.

React Transition API и графики

При обновлении данных графики могут резко мигать.

React 18 позволяет использовать:

startTransition()

Пример

import { startTransition } from 'react'

function updateChart(data) {
    startTransition(() => {
        setChartData(data)
    })
}

Польза для Nivo

Во время тяжёлого рендера:

  • интерфейс не блокируется;
  • input остаётся отзывчивым;
  • dashboard не зависает.

Suspense для tab-based dashboard

Частая архитектура:

<Tabs>
    <AnalyticsTab />
    <ReportsTab />
    <FinanceTab />
</Tabs>

Каждая вкладка может грузить графики отдельно.


Lazy tabs

const AnalyticsTab = lazy(() =>
    import('./AnalyticsTab')
)

Использование

<Suspense fallback={<TabLoader />}>
    <AnalyticsTab />
</Suspense>

Prefetch графиков

Для ускорения UX можно предварительно загружать chunk.


Пример

import('./AnalyticsChart')

до открытия страницы.


ErrorBoundary не ловит

Важно понимать ограничения.

ErrorBoundary НЕ перехватывает:

  • ошибки event handlers;
  • async/await exceptions;
  • setTimeout;
  • fetch errors вне render phase.

Пример

button.oncl ick = async () => {
    throw new Error()
}

ErrorBoundary не сработает.


Правильная обработка async ошибок

try {
    const data = await fetchData()
} catch (e) {
    setError(e)
}

Локальное состояние ошибки

if (error) {
    return <ErrorState />
}

Комбинация локальной ошибки и ErrorBoundary

На практике используются оба механизма:

<ErrorBoundary>
    <ChartContainer />
</ErrorBoundary>

и внутри:

if (loading) {
    return <Loader />
}

if (error) {
    return <ErrorMessage />
}

Архитектура production-grade dashboard

Типичная структура:

<ErrorBoundary>

    <Suspense fallback={<DashboardSkeleton />}>

        <DashboardLayout>

            <Suspense fallback={<ChartSkeleton />}>
                <RevenueChart />
            </Suspense>

            <Suspense fallback={<ChartSkeleton />}>
                <UsersChart />
            </Suspense>

            <Suspense fallback={<ChartSkeleton />}>
                <MapChart />
            </Suspense>

        </DashboardLayout>

    </Suspense>

</ErrorBoundary>

Практические рекомендации

Использовать lazy loading для тяжёлых графиков

Особенно:

  • maps;
  • network;
  • canvas;
  • large datasets.

Изолировать каждый крупный chart

<ErrorBoundary>
    <Chart />
</ErrorBoundary>

Всегда задавать размеры контейнера

height: 400

Валидировать API-данные

До передачи в Nivo.


Не использовать один Suspense на весь dashboard

Это ухудшает UX.


Использовать skeleton вместо текста загрузки

Так интерфейс выглядит стабильнее.


Логировать ошибки в production

Через Sentry или аналогичные системы.


Разделять SVG и Canvas-графики

Canvas требует более агрессивной изоляции ошибок.