Библиотека Nivo активно использует React-компоненты и хорошо сочетается с современными механизмами асинхронного рендеринга. При работе с удалёнными источниками данных, ленивой загрузкой графиков и динамическими dashboard-интерфейсами особенно важны:
Для решения этих задач используются:
React.Suspense;React.lazy;ErrorBoundary.Компоненты Nivo могут:
Без изоляции ошибок возникают типичные проблемы:
<ResponsiveBar data={undefined} />
Результат:
TypeError: Cannot read property 'map' of undefined
Ошибка внутри одного графика способна уничтожить весь React tree.
Suspense позволяет показывать fallback-интерфейс во
время:
Простейшая схема:
<Suspense fallback={<Loader />}>
<Chart />
</Suspense>
Пока компонент загружается — отображается Loader.
Графики 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>
)
}
React:
BarChart;fallback;Особенно критично для dashboard-приложений.
Без lazy loading:
main.js = 2.4 MB
С lazy loading:
main.js = 620 KB
Интерфейс становится интерактивным раньше.
Каждый chart может подгружаться отдельно.
Каждый график желательно изолировать.
<Suspense fallback={<PageLoader />}>
<SalesChart />
<UsersChart />
<RevenueChart />
</Suspense>
Если один график грузится долго — блокируются все.
<>
<Suspense fallback={<ChartLoader />}>
<SalesChart />
</Suspense>
<Suspense fallback={<ChartLoader />}>
<UsersChart />
</Suspense>
<Suspense fallback={<ChartLoader />}>
<RevenueChart />
</Suspense>
</>
Обычный текст выглядит плохо.
function ChartSkeleton() {
return (
<div
style={{
height: 400,
borderRadius: 12,
background: '#f3f3f3'
}}
/>
)
}
<Suspense fallback={<ChartSkeleton />}>
<BarChart />
</Suspense>
Nivo ожидает корректные данные.
Ошибки возникают при:
null;undefined;keys;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
}
}
<ErrorBoundary>
<ResponsiveBar {...props} />
</ErrorBoundary>
Теперь сбой графика не уничтожит приложение.
Наиболее распространённая архитектура:
<ErrorBoundary>
<Suspense fallback={<ChartSkeleton />}>
<SalesChart />
</Suspense>
</ErrorBoundary>
Правильный порядок:
<ErrorBoundary>
<Suspense>
<Chart />
</Suspense>
</ErrorBoundary>
ErrorBoundary должен перехватывать:
Если разместить наоборот:
<Suspense>
<ErrorBoundary>
<Chart />
</ErrorBoundary>
</Suspense>
часть ошибок останется непойманной.
Частая ситуация:
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>
}
С React 18 появились patterns для data fetching через Suspense.
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"
/>
)
}
<ErrorBoundary>
<Suspense fallback={<ChartSkeleton />}>
<Chart />
</Suspense>
</ErrorBoundary>
Если promise:
ErrorBoundary можно сбрасывать.
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
}
}
Самостоятельная реализация не всегда удобна.
Популярное решение:
npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary'
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>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onRe set={() => {
reloadChartData()
}}
>
<Chart />
</ErrorBoundary>
Canvas-версии Nivo:
ResponsiveBarCanvas;ResponsiveLineCanvas;ResponsiveScatterPlotCanvas;чаще вызывают runtime errors при:
<ErrorBoundary>
<ResponsiveLineCanvas {...props} />
</ErrorBoundary>
Это особенно важно для аналитических систем.
При использовании:
Suspense становится частью серверного рендеринга.
import dynamic from 'next/dynamic'
const Chart = dynamic(
() => import('./Chart'),
{
ssr: false,
loading: () => <ChartSkeleton />
}
)
Некоторые компоненты используют:
window
document
ResizeObserver
На сервере этих объектов нет.
const Chart = dynamic(
() => import('./Chart'),
{
ssr: false
}
)
<ErrorBoundary>
<Chart />
</ErrorBoundary>
Крупные dashboard лучше сегментировать.
<Dashboard>
<ErrorBoundary>
<SalesCharts />
</ErrorBoundary>
<ErrorBoundary>
<MarketingCharts />
</ErrorBoundary>
<ErrorBoundary>
<FinanceCharts />
</ErrorBoundary>
</Dashboard>
При сбое:
ErrorBoundary позволяет подключать:
componentDidCatch(error, info) {
Sentry.captureException(error, {
extra: info
})
}
keys={['revenue']}
Но данные:
{
sales: 120
}
data={undefined}
<div>
<ResponsiveBar />
</div>
Без высоты:
height: 0
<div
style={{
height: 400
}}
>
<ResponsiveBar />
</div>
Иногда вместо падения лучше отображать резервный UI.
function EmptyChartState() {
return (
<div>
Данные временно недоступны
</div>
)
}
<ErrorBoundary
fallback={<EmptyChartState />}
>
<Chart />
</ErrorBoundary>
Плохая практика:
<Suspense>
<ChartPart1 />
</Suspense>
<Suspense>
<ChartPart2 />
</Suspense>
<Suspense>
<ChartPart3 />
</Suspense>
Возникают:
Лучше оборачивать:
При обновлении данных графики могут резко мигать.
React 18 позволяет использовать:
startTransition()
import { startTransition } from 'react'
function updateChart(data) {
startTransition(() => {
setChartData(data)
})
}
Во время тяжёлого рендера:
Частая архитектура:
<Tabs>
<AnalyticsTab />
<ReportsTab />
<FinanceTab />
</Tabs>
Каждая вкладка может грузить графики отдельно.
const AnalyticsTab = lazy(() =>
import('./AnalyticsTab')
)
<Suspense fallback={<TabLoader />}>
<AnalyticsTab />
</Suspense>
Для ускорения UX можно предварительно загружать chunk.
import('./AnalyticsChart')
до открытия страницы.
Важно понимать ограничения.
ErrorBoundary НЕ перехватывает:
button.oncl ick = async () => {
throw new Error()
}
ErrorBoundary не сработает.
try {
const data = await fetchData()
} catch (e) {
setError(e)
}
if (error) {
return <ErrorState />
}
На практике используются оба механизма:
<ErrorBoundary>
<ChartContainer />
</ErrorBoundary>
и внутри:
if (loading) {
return <Loader />
}
if (error) {
return <ErrorMessage />
}
Типичная структура:
<ErrorBoundary>
<Suspense fallback={<DashboardSkeleton />}>
<DashboardLayout>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<UsersChart />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<MapChart />
</Suspense>
</DashboardLayout>
</Suspense>
</ErrorBoundary>
Особенно:
<ErrorBoundary>
<Chart />
</ErrorBoundary>
height: 400
До передачи в Nivo.
Это ухудшает UX.
Так интерфейс выглядит стабильнее.
Через Sentry или аналогичные системы.
Canvas требует более агрессивной изоляции ошибок.